// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/huobi/api_order/native_private_client.h"

#include <list>
#include <memory>
#include <string>

#include <rapidjson/writer.h>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/huobi/symbology/product.h"

using coin2::exchange::huobi::symbology::HuobiProduct;
using AsioHttpContextList = std::list<std::shared_ptr<AsioHttpContext>>;

auto HuobiNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  HuobiProduct product = HuobiProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  params.set_symbol(order_spec.product);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_price(formatter.FormatPrice(order_spec.price));
  params.set_amount(formatter.FormatQty(order_spec.qty));
  params.set_client_order_id(std::to_string(proc_order_id));

  if (order_spec.post_only) {
    if (order_spec.order_side == OrderSide::BUY_ORDER) {
      params.set_type("buy-limit-maker");
    } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
      params.set_type("sell-limit-maker");
    } else {
      NOTREACHED() << "Unknown post_only order side: " << static_cast<int>(order_spec.order_side);
    }
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    if (order_spec.order_side == OrderSide::BUY_ORDER) {
      params.set_type("buy-ioc");
    } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
      params.set_type("sell-ioc");
    } else {
      NOTREACHED() << "Unknown ioc order side: " << static_cast<int>(order_spec.order_side);
    }
  } else if (order_spec.order_type == coin::proto::OrderType::MARKET_ORDER) {
    if (order_spec.order_side == OrderSide::BUY_ORDER) {
      params.set_type("buy-market");
    } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
      params.set_type("sell-market");
    } else {
      NOTREACHED() << "Unknown market order side: " << static_cast<int>(order_spec.order_side);
    }
  } else if (order_spec.order_type == coin::proto::OrderType::LIMIT_ORDER) {
    if (order_spec.order_side == OrderSide::BUY_ORDER) {
      params.set_type("buy-limit");
    } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
      params.set_type("sell-limit");
    } else {
      NOTREACHED() << "Unknown limit order side: " << static_cast<int>(order_spec.order_side);
    }
  } else {
    NOTREACHED() << "Unknown order type: " << static_cast<int>(order_spec.order_type);
  }
  return params;
}

auto HuobiNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id());
  } else {
    params.set_client_order_id(std::to_string(order_context.proc_order_id()));
  }
  return params;
}

HuobiNativePrivateClient::HuobiNativePrivateClient(const NativeClientConfig& config_override) {
  NativeClientConfig config;
  config.set_rest_host("api.huobi.pro");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[HuobiNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<HuobiRestAuth, NativeClientConfig>(config);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryAccounts() {
  CHECK(mb_);
  const std::string ch_id_type("huobi/account_type_id");
  if (!mb_->has_channel(ch_id_type)) {
    mb_->Subscribe(
        ch_id_type,
        std::bind(
            &HuobiNativePrivateClient::SetAccountIdTypeMap,
            this,
            std::placeholders::_1,
            std::placeholders::_2));
  }
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/v1/account/accounts", &builder);

  context->query_type = OrderSubTopicId::REST_ACCOUNTS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryAccountBalance() {
  if (!has_account_id()) {
    LOG(ERROR) << "account_id not ready!";
    return {};
  }
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature(
      "/v1/account/accounts/" + account_id_ + "/balance",
      &builder);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryMarginCrossedAccountBalance() {
  if (!has_margin_crossed_account_id()) {
    LOG(ERROR) << "margin_crossed_account_id not ready!";
    return {};
  }
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature(
      "/v1/account/accounts/" + margin_crossed_account_id_ + "/balance",
      &builder);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_CROSS_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryMarginIsolatedAccountBalance() {
  if (!has_margin_isolated_account_id()) {
    LOG(ERROR) << "margin_isolated_account_id not ready!";
    return {};
  }
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature(
      "/v1/account/accounts/" + margin_isolated_account_id_ + "/balance",
      &builder);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_NON_CROSS_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryDepositWithdraw(
    const RestParams& params) {
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_type()) << "Huobi QueryDepositWithdraw params missing type";
  builder.PutGet("currency", params.currency());
  builder.PutGet("type", params.type());
  builder.PutGet("from", params.from());
  builder.PutGet("direct", params.direct());
  builder.PutGet("size", params.size());

  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/v1/query/deposit-withdraw", &builder);
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryOrders(const RestParams& params) {
  CHECK(false);
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_states()) << "Huobi QueryOrders params missing states";
  CHECK(params.has_symbol()) << "Huobi QueryOrders params missing symbol";
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("types", params.types());
  builder.PutGet("start-date", params.start_date());
  builder.PutGet("end-date", params.end_date());
  builder.PutGet("states", params.states());
  builder.PutGet("from", params.from());
  builder.PutGet("direct", params.direct());
  builder.PutGet("size", params.size());

  auto* context = GetAuth()->CreateRequestByGetWithSignature("/v1/order/orders", &builder);
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QuerySingleOrder(
    const RestParams& params) {
  CHECK(false);
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_order_id()) << "Huobi QuerySingleOrder params missing order_id";
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/v1/order/orders/" + params.order_id(), &builder);
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryOpenOrders(
    const RestParams& params) {
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_symbol()) << "Huobi QueryOpenOrders params missing symbol";
  builder.PutGet("account-id", account_id_);
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("side", params.side());
  builder.PutGet("from", params.from());
  builder.PutGet("direct", params.direct());
  builder.PutGet("size", params.size());

  auto* context = GetAuth()->CreateRequestByGetWithSignature("/v1/order/openOrders", &builder);

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryHistoryOrders(
    const RestParams& params) {
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;

  builder.PutGet("symbol", params.symbol());
  builder.PutGet("direct", params.direct());
  builder.PutGet("size", params.size());
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/v1/order/history", &builder);

  context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryOrderMatchResults(
    const RestParams& params) {
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_symbol()) << "Huobi QueryOrderMatchResults params missing symbol";
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("types", params.types());
  builder.PutGet("start-date", params.start_date());
  builder.PutGet("end-date", params.end_date());
  builder.PutGet("from", params.from());
  builder.PutGet("direct", params.direct());
  builder.PutGet("size", params.size());

  auto* context = GetAuth()->CreateRequestByGetWithSignature("/v1/order/matchresults", &builder);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QuerySingleOrderMatchResults(
    const RestParams& params) {
  CHECK(false);
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_order_id()) << "Huobi QuerySingleOrderMatchResults params missing order_id";
  auto* context = GetAuth()->CreateRequestByGetWithSignature(
      "/v1/order/orders/" + params.order_id() + "/matchresults",
      &builder);
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::PlaceSingleOrder(
    const RestParams& params) {
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_symbol()) << "Huobi PlaceSingleOrder params missing symbol";
  CHECK(params.has_type()) << "Huobi PlaceSingleOrder params missing type";
  CHECK(params.has_amount()) << "Huobi PlaceSingleOrder params missing amount";
  builder.PutPost("account-id", account_id_);
  builder.PutPost("symbol", params.symbol());
  builder.PutPost("type", params.type());
  builder.PutPost("amount", params.amount());
  builder.PutPost("price", params.price());
  builder.PutPost("source", params.source());
  builder.PutPost("client-order-id", params.client_order_id());
  builder.PutPost("stop-price", params.stop_price());
  builder.PutPost("operator", params.operator_());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/v1/order/orders/place", &builder);

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::PlaceBatchOrders(
    const std::vector<RestParams>& params) {
  if (!has_account_id()) {
    return {};
  }

  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartArray();

  for (const auto& param : params) {
    CHECK(param.has_symbol()) << "Huobi PlaceBatchOrders params missing symbol";
    CHECK(param.has_type()) << "Huobi PlaceBatchOrders params missing type";
    CHECK(param.has_amount()) << "Huobi PlaceBatchOrders params missing amount";

    writer.StartObject();

    writer.Key("account-id");
    writer.String(account_id_.c_str());
    writer.Key("symbol");
    writer.String(param.symbol().c_str());
    writer.Key("type");
    writer.String(param.type().c_str());
    writer.Key("amount");
    writer.String(param.amount().c_str());
    writer.Key("price");
    writer.String(param.price().c_str());
    writer.Key("client-order-id");
    writer.String(param.client_order_id().c_str());
    if (!param.source().empty()) {
      writer.Key("source");
      writer.String(param.source().c_str());
    }
    if (!param.stop_price().empty()) {
      writer.Key("stop-price");
      writer.String(param.stop_price().c_str());
    }
    if (!param.operator_().empty()) {
      writer.Key("operator");
      writer.String(param.operator_().c_str());
    }

    writer.EndObject();
  }

  writer.EndArray();

  auto* context = static_cast<HuobiRestAuth*>(GetAuth())->CreateRequestByPostWithSignature(
      "/v1/order/batch-orders",
      str_buf.GetString());

  context->query_type = OrderSubTopicId::MULTI_SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::CancelOrder(const RestParams& params) {
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;
  CHECK(params.has_order_id() != params.has_client_order_id())
      << "only one of order_id and client_order_id can be set!" << params.DebugString();

  if (params.has_order_id() > 0) {
    auto* context = GetAuth()->CreateRequestByPostWithSignature(
        "/v1/order/orders/" + params.order_id() + "/submitcancel",
        &builder);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  } else {
    builder.PutPost("client-order-id", params.client_order_id());
    auto* context = GetAuth()->CreateRequestByPostWithSignature(
        "/v1/order/orders/submitCancelClientOrder",
        &builder);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  }
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::BatchCancelOrders(
    const RestParams& params) {
  if (!has_account_id()) {
    return {};
  }

  CHECK(params.order_ids().empty() != params.client_order_ids().empty())
      << "only one of order_ids and client_order_ids can be set!" << params.DebugString();

  UrlParamsBuilder builder;
  std::list<std::string> order_str_list;
  std::list<std::string> client_order_str_list;
  int size = params.order_ids_size();
  for (int i = 0; i < size; i++) {
    order_str_list.push_back(params.order_ids(i));
  }
  builder.PutPost<true>("order-ids", order_str_list);
  size = params.client_order_ids_size();
  for (int i = 0; i < size; i++) {
    client_order_str_list.push_back(params.client_order_ids(i));
  }
  builder.PutPost<true>("client-order-ids", client_order_str_list);
  auto* context =
      GetAuth()->CreateRequestByPostWithSignature("/v1/order/orders/batchcancel", &builder);

  context->query_type = OrderSubTopicId::MULTI_CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::BatchCancelOpenOrders(
    const RestParams& params) {
  if (!has_account_id()) {
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutPost("account-id", account_id_);
  builder.PutPost("symbol", params.symbol());
  builder.PutPost("side", params.side());
  builder.PutPost("size", params.size());
  auto* context = GetAuth()->CreateRequestByPostWithSignature(
      "/v1/order/orders/batchCancelOpenOrders",
      &builder);
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryDeposit() {
  UrlParamsBuilder builder;
  builder.PutGet("type", "deposit");
  builder.PutGet("size", "500");
  builder.PutGet("direct", "next");
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/v1/query/deposit-withdraw", &builder);

  context->query_type = HuobiOrderSubTopicId::REST_DEPOSIT;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryWithdrawal() {
  UrlParamsBuilder builder;
  builder.PutGet("type", "withdraw");
  builder.PutGet("size", "500");
  builder.PutGet("direct", "next");
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/v1/query/deposit-withdraw", &builder);

  context->query_type = HuobiOrderSubTopicId::REST_WITHDRAWAL;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::QueryTransfers(
    const RestParams& params) {
  UrlParamsBuilder builder;

  CHECK(params.has_account_id()) << "account_id not set!" << params.DebugString();

  if (params.has_start_date() && params.has_end_date()) {
    builder.PutGet("startTime", params.start_date());
    builder.PutGet("endTime", params.end_date());
  }

  builder.PutGet("accountId", params.account_id());
  builder.PutGet("transactTypes", "transfer");

  auto* context = GetAuth()->CreateRequestByGetWithSignature("/v2/account/ledger", &builder);
  context->query_type = OrderSubTopicId::TRANSFER;
  context->extra_info = account_id();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  if (!order_exe.is_place_batch_orders) {
    const auto& order_spec = *order_exe.order_spec;
    const auto proc_order_id = order_exe.proc_order_id;
    auto params = GetRestOrderParams(order_spec, proc_order_id);
    return PlaceSingleOrder(params);
  }

  auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

  if (mutable_order_exe->place_pending_order_deque.empty()) {
    return {};
  }

  std::vector<RestParams> params;
  for (int i = 0; i < 10 && !mutable_order_exe->place_pending_order_deque.empty(); ++i) {
    const auto* order_spec = mutable_order_exe->place_pending_order_deque.front();
    mutable_order_exe->place_pending_order_deque.pop_front();

    mutable_order_exe->place_proceed_order_deque.push_back(order_spec);

    params.emplace_back(GetRestOrderParams(*order_spec, order_spec->proc_order_id));
  }

  return PlaceBatchOrders(params);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  if (!order_exe.is_cancel_batch_orders) {
    auto params = GetRestCancelParams(*order_exe.oc);
    return CancelOrder(params);
  }

  auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

  if (mutable_order_exe->cancel_pending_order_deque.empty()) {
    return {};
  }

  RestParams param;
  for (int i = 0; i < 50 && !mutable_order_exe->cancel_pending_order_deque.empty(); ++i) {
    const auto* order_context = mutable_order_exe->cancel_pending_order_deque.front();
    mutable_order_exe->cancel_pending_order_deque.pop_front();

    mutable_order_exe->cancel_proceed_order_deque.push_back(order_context);

    if (order_context->has_exchange_order_id()) {
      param.add_order_ids(order_context->exchange_order_id());
    }

    param.add_client_order_ids(std::to_string(order_context->proc_order_id()));
  }

  if (param.order_ids_size() < param.client_order_ids_size()) {
    param.clear_order_ids();
  } else {
    param.clear_client_order_ids();
  }

  return BatchCancelOrders(param);
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceSingleOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> HuobiNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountBalance();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryOpenOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}

AsioHttpContextList HuobiNativePrivateClient::SubmitRestQueryList(const RestQueryProto& query) {
  AsioHttpContextList list;
  switch (query.type()) {
    case RestQueryProto::INIT_QUERY_INFO: {
      list.emplace_back(QueryAccounts());
      break;
    }
    case RestQueryProto::QUERY_TRANSFERS: {
      RestParams params;
      params.set_start_date(std::to_string(query.start_time() / 1000'000));
      params.set_end_date(std::to_string(query.end_time() / 1000'000));
      const auto& account_type_map = GetAccountIdTypeMap();
      for (const auto& account : account_type_map) {
        params.set_account_id(account.second);
        list.emplace_back(QueryTransfers(params));
      }
      break;
    }
    case RestQueryProto::QUERY_DEPOSIT: {
      list.emplace_back(QueryDeposit());
      break;
    }
    case RestQueryProto::QUERY_WITHDRAW: {
      list.emplace_back(QueryWithdrawal());
      break;
    }
    default: {
      return list;
    }
  }

  return list;
}